Welcome to the second part in our blog post series about Thorgate’s new SPA project template! In the first part we discussed our template a little bit and generated a new project based on the template. Now that you have a general overview of what the project structure is like, we can start building our small application.
Here are the links to the other posts:
As mentioned in the previous article, I would like to create a simple parrot reference web app that can be used to keep track of interesting parrots. Each user can add parrots and see a list of their parrots. We’ll ensure that the server-side rendering works and make use of some of the interesting technologies included in the project template.
Showing parrots
Let’s start with showing the user their saved parrots. Since we already have users and authentication set up, we don’t need to put too much effort into getting the current user’s personal parrots.
Firstly, it’s a good idea to create a superuser account for development. We would normally call python manage.py createsuperuser
to create a superuser in Django, but since we’re running everything inside Docker, then the command would be a bit longer. Something like this would do the trick: docker-compose run --rm django python manage.py createsuperuser
. But who can remember that? In order to make it easier for everyone, the Makefile
includes a useful alias for running management commands: docker-manage
. We can create a new superuser like this: make docker-manage cmd="createsuperuser"
. Now we can either click on “Admin panel” in the navbar or go to http://127.0.0.1:8000/adminpanel/ to see the Django admin page and log in.
Setting up models and Django admin
We need to stick our parrot-management logic somewhere, so let’s create a new app: parrots
.
$ make docker-manage cmd="startapp parrots"
Since the folder is created inside the Docker container, it is possible that its permissions are incorrect. We can fix that by running the following command:
$ sudo chown -R "$(id -un):$(id -gn)" parrot_mania
We will also need to add our newly generated app to our INSTALLED_APPS
in settings/base.py
:
# parrot_mania/settings/base.py INSTALLED_APPS = [ # Local apps 'accounts', 'parrot_mania', 'parrots', ... ]
Now we can add our Parrot
model in parrots/models.py
:
# parrot_mania/parrots/models.py from django.db import models from accounts.models import User class Parrot(models.Model): name = models.CharField(max_length=255) link = models.TextField() user = models.ForeignKey(User)
And let’s register the Parrot
model to our admin site in parrots/admin.py
as well:
# parrot_mania/parrots/admin.py from django.contrib import admin from parrots.models import Parrot admin.site.register(Parrot)
And let’s migrate our database using aliases from the Makefile
. Use the following commands in your terminal:
$ make makemigrations $ make migrate
Now we can add parrots through the Django admin.
Showing hard-coded parrots in the front-end
The next step is to create a page on the front-end where we will show the parrots. At first, let’s use some dummy data so that we can focus on just the React piece of the puzzle. Create a file at app/src/views/ParrotsList.js
:
# app/src/views/ParrotsList.js
import React from 'react';
import { Container } from 'reactstrap';
import withView from 'decorators/withView';
const exampleParrots = [
{ id: 1, name: 'Alex', link: 'https://www.youtube.com/user/doorbell26' },
{ id: 2, name: 'Benjamin', link: 'https://www.youtube.com/user/parrotpost' },
];
const ParrotsList = () => (
<Container>
<h1>Here are your parrots!</h1>
<ul>
{exampleParrots.map((parrot) => (
<li key={parrot.id}>
<a href={parrot.link}>{parrot.name}</a>
</li>
))}
</ul>
</Container>
);
export default withView()(ParrotsList);
But how do we specify that this is an actual page that React should render on a specific URL? Well, we need to specify this in the src/configuration/routes.js
file.
Add the following object toward the end of the existing routes, just above the last NotFoundRoute
:
# app/src/configuration/routes.js
...
{
path: '/parrots',
exact: true,
name: 'parrots-list',
component: ParrotsList,
},
NotFoundRoute,
We also need to import the component though, so let’s add this to the top of the file:
# app/src/configuration/routes.js
const ParrotsList = loadable(() => import('views/ParrotsList'));
This funny import handles code-splitting, dynamically loading only the necessary JavaScript for the page that the user is currently on.
Now we can navigate to http://127.0.0.1:8000/parrots and see our example parrots.
This is a good time to ensure that our server-side rendering works, we can curl
the /parrots
page or right click and “View page source”:
$ curl http://127.0.0.1:8000/parrots ... <h1>Here are your parrots!</h1> ...
Even though the HTML is minified, we can see that it includes our example parrots and their links.
API for parrots using Django Rest Framework
So far it has been a pretty standard Django and React application, but now that we need to show dynamic data to the user, we need communication between the server and the client. Let’s first do this via standard HTTP requests and then bring in Redux Saga later.
Firstly, we need an API endpoint to fetch parrots. Using Django Rest Framework, it is possible to set up API endpoints very quickly. Let’s create our DRF serializer in parrot_mania/parrots/serializers.py
:
# parrot_mania/parrots/serializers.py
from rest_framework import serializers
from parrots.models import Parrot
class ParrotSerializer(serializers.ModelSerializer):
class Meta:
model = Parrot
fields = ('id', 'name', 'link')
And our viewset in parrot_mania/parrots/views.py
:
# parrot_mania/parrots/views.py
from rest_framework import viewsets
from rest_framework.permissions import IsAuthenticated
from parrots.models import Parrot
from parrots.serializers import ParrotSerializer
class ParrotViewSet(viewsets.ModelViewSet):
queryset = Parrot.objects.all()
serializer_class = ParrotSerializer
permission_classes = [IsAuthenticated]
def get_queryset(self):
"""Return the authenticated user's parrots."""
return super().get_queryset().filter(user=self.request.user)
And register the viewset’s URLs using Django Rest Framework’s router in
parrot_mania/parrots/urls.py
:
# parrot_mania/parrots/urls.py
from rest_framework import routers
from parrots.views import ParrotViewSet
router = routers.SimpleRouter()
router.register(r'parrots', ParrotViewSet, base_name='parrots')
urlpatterns = router.urls
And finally include these URLs in parrot_mania/parrot_mania/rest/urls.py
:
# parrot_mania/parrot_mania/rest/urls.py
urlpatterns = [
...
url(r'^', include('parrots.urls')),
]
We can test it out with curl
if we temporarily remove the authentication check and current user filtering:
# parrot_mania/parrots/views.py
class ParrotViewSet(viewsets.ModelViewSet):
queryset = Parrot.objects.all()
serializer_class = ParrotSerializer
# permission_classes = [IsAuthenticated]
# def get_queryset(self):
# return super().get_queryset().filter(user=self.request.user)
And the curl
command:
$ curl http://127.0.0.1:8000/api/parrots/ [ {"id":1,"name":"Alex","link":"https://www.youtube.com/user/doorbell26"}, {"id":2,"name":"Benjamin","link":"https://www.youtube.com/user/parrotpost"} ]
Fetching the parrots in the front-end
We need to get the data about parrots to our front-end though. At first, let’s try to do this with a simple fetch
request:
# app/src/views/ParrotsList.js
import React, { Component } from 'react';
import { Container } from 'reactstrap';
import withView from 'decorators/withView';
class ParrotsList extends Component {
state = {
parrots: [],
};
componentDidMount() {
fetch('/api/parrots')
.then((res) => res.json())
.then((parrots) => {
this.setState({ parrots });
});
}
render() {
return (
<Container>
<h1>Here are your parrots!</h1>
<ul>
{this.state.parrots.map((parrot) => (
<li key={parrot.id}>
<a href={parrot.link}>{parrot.name}</a>
</li>
))}
</ul>
</Container>
);
}
}
export default withView()(ParrotsList);
The problem here, is that the parrots no longer get server-rendered as they are fetched in componentDidMount
— when the component has been rendered on the client side. In order to fix this, you can write your first saga. A saga listens to any dispatched Redux actions and can react to them — for example, dispatching more actions or making API requests. In our SPA projects, all communication with the API goes through sagas.
The saga we will write will be very simple. It’s going to be called whenever anyone navigates to the /parrots
route. In addition, it will be called on the server whenever the user initially navigates to the /parrots
route.
At first, let’s just log something to the console so that we see that it’s working. Create an app/src/sagas/parrots
directory and a fetchUserParrots.js
file inside it with the following contents (ensure not to miss the *
next to function
as it’s a generator function):
# app/src/sagas/parrots/fetchUserParrots.js
export default function* fetchUserParrots() {
console.log('Fetching some parrots!');
}
We also need to configure this saga to be triggered whenever users visit the /parrots
page. In order to do that, we can import the saga and modify our route configuration in configuration/routes.js
:
# app/src/configuration/routes.js
import fetchUserParrots from 'sagas/parrots/fetchUserParrots';
...
{
path: '/parrots',
exact: true,
name: 'parrots-list',
component: ParrotsList,
initial: [
fetchUserParrots,
],
},
Now we should see the message logged in the console.
Sagas offer a way of handling side effects in Redux projects (like API requests) that are easy to manage and test. Redux Saga has a nice introduction on their website at https://redux-saga.js.org.
In our project template, the sagas specified in the initial
array for each route are executed whenever a user visits the route. They’re also executed on the server-side so that data can be fetched on the server and returned to the user when they first query the page.
Now that we have a function that’s triggered when the user queries /parrots
, we can add API request logic there. But before that, let’s create a Redux duck to contain the parrots. Create the app/src/ducks/parrots.js
file and add the following:
# app/src/ducks/parrots.js
import { combineReducers } from 'redux';
export const RECEIVE_PARROTS = 'parrots/RECEIVE_PARROTS';
const parrotsReducer = (state = [], action) => {
switch (action.type) {
case RECEIVE_PARROTS:
return action.parrots;
default:
return state;
}
};
export default combineReducers({
parrots: parrotsReducer,
});
// Action creators
export const receiveParrots = (parrots) => ({
type: RECEIVE_PARROTS,
parrots,
});
We also need to configure our application to use the parrots
duck. We can do that in configuration/reducers.js
:
# app/src/configuration/reducers.js
import parrots from 'ducks/parrots';
export default (history) => combineReducers({
...
parrots,
});
Finally, we can connect the ParrotsList
component to the Redux store:
# app/src/views/ParrotsList.js
import React from 'react';
import { Container } from 'reactstrap';
import { connect } from 'react-redux';
import withView from 'decorators/withView';
const ParrotsList = ({ parrots }) => (
<Container>
<h1>Here are your parrots!</h1>
<ul>
{parrots.map((parrot) => (
<li key={parrot.id}>
<a href={parrot.link}>{parrot.name}</a>
</li>
))}
</ul>
</Container>
);
const mapStateToProps = (state) => ({
parrots: state.parrots.parrots,
});
const ParrotsListConnector = connect(
mapStateToProps,
)(ParrotsList);
export default withView()(ParrotsListConnector);
It is now possible to dispatch RECEIVE_PARROTS
actions with some parrots and view them. Let’s quickly try this out without any API requests just to ensure everything works alright. Edit the fetchUserParrots
saga:
# app/src/sagas/parrots/fetchUserParrots.js
import { put } from 'redux-saga/effects';
import { receiveParrots } from 'ducks/parrots';
export default function* fetchUserParrots() {
yield put(receiveParrots([
{ id: 1, name: 'Alex', link: 'https://www.youtube.com/user/doorbell26' },
{ id: 2, name: 'Benjamin', link: 'https://www.youtube.com/user/parrotpost' },
]));
}
And we should now be able to see the hard-coded parrots again. We dispatch the RECEIVE_PARROTS
action using the put
function from Redux Saga. The next thing to do is to make an API request to retrieve the user’s parrots and dispatch the action using those parrots instead.
We could use fetch
to make this API request, but we have a helper library for making API requests called tg-resources
(which uses fetch
or superagent
under the hood). Using tg-resources
, we can configure a “resource” that can be used to easily make API calls. Let’s configure the parrotsList
resource in services/api.js
. In the createSagaRouter
arguments, add the parrots
resource like so:
# app/src/services/api.js
const api = createSagaRouter({
...
parrots: {
list: 'parrots/',
},
}, {
...headers, apiRoot settings, etc.
});
We can now make use of this resource in the fetchUserParrots
Saga:
# app/src/sagas/parrots/fetchUserParrots.js
import { put } from 'redux-saga/effects';
import { receiveParrots } from 'ducks/parrots';
import api from 'services/api';
export default function* fetchUserParrots() {
try {
const parrots = yield api.parrots.list.fetch();
yield put(receiveParrots(parrots));
} catch (err) {
console.error('Something went wrong!');
}
}
We call the fetch
method on the api.parrots.list
resource triggering a GET request to /api/parrots/
. Similarly, we could use post
to make a POST request with some data.
The saved parrots should now show up on the page and in the HTML that the server sends to the client. This can be double checked by right clicking on the page and selecting “View page source”.
I created a simple graphic to show how server-side rendering works in the /parrots
view:
Server-side rendering allows search engine crawlers to index pages while we still write all rendering using React.
Next steps
In this post we showed the user a list of their parrots. The parrots can only be added by superusers in Django admin though, so let’s add a more convenient way to save parrots in the next blog post. We'll be using Formik and Redux Saga to create a form for adding parrots and to send the request to create a parrot to the server.
This has been the second article in a series of blog posts about Thorgate’s new SPA project template. Check out the next post here: